// Copyright 2015 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

#include "content/browser/loader/async_revalidation_manager.h"

#include <deque>
#include <string>
#include <utility>

#include "base/bind.h"
#include "base/callback.h"
#include "base/macros.h"
#include "base/memory/ptr_util.h"
#include "base/memory/shared_memory_handle.h"
#include "base/pickle.h"
#include "base/run_loop.h"
#include "base/strings/string_util.h"
#include "content/browser/child_process_security_policy_impl.h"
#include "content/browser/loader/resource_dispatcher_host_impl.h"
#include "content/browser/loader/resource_message_filter.h"
#include "content/browser/loader_delegate_impl.h"
#include "content/common/child_process_host_impl.h"
#include "content/common/resource_messages.h"
#include "content/common/resource_request.h"
#include "content/public/browser/resource_context.h"
#include "content/public/common/appcache_info.h"
#include "content/public/common/resource_type.h"
#include "content/public/test/test_browser_context.h"
#include "content/public/test/test_browser_thread_bundle.h"
#include "ipc/ipc_param_traits.h"
#include "net/base/load_flags.h"
#include "net/base/network_delegate.h"
#include "net/http/http_util.h"
#include "net/url_request/url_request.h"
#include "net/url_request/url_request_job.h"
#include "net/url_request/url_request_job_factory.h"
#include "net/url_request/url_request_test_job.h"
#include "net/url_request/url_request_test_util.h"
#include "testing/gtest/include/gtest/gtest.h"
#include "ui/base/page_transition_types.h"
#include "url/gurl.h"
#include "url/url_constants.h"

namespace content {

namespace {

    // This class is a variation on URLRequestTestJob that
    // returns ERR_IO_PENDING before every read, not just the first one.
    class URLRequestTestDelayedCompletionJob : public net::URLRequestTestJob {
    public:
        URLRequestTestDelayedCompletionJob(net::URLRequest* request,
            net::NetworkDelegate* network_delegate,
            const std::string& response_headers,
            const std::string& response_data)
            : net::URLRequestTestJob(request,
                network_delegate,
                response_headers,
                response_data,
                false)
        {
        }

    private:
        bool NextReadAsync() override { return true; }
    };

    // A URLRequestJob implementation which sets the |async_revalidation_required|
    // flag on the HttpResponseInfo object to true if the request has the
    // LOAD_SUPPORT_ASYNC_REVALIDATION flag.
    class AsyncRevalidationRequiredURLRequestTestJob
        : public net::URLRequestTestJob {
    public:
        AsyncRevalidationRequiredURLRequestTestJob(
            net::URLRequest* request,
            net::NetworkDelegate* network_delegate)
            : URLRequestTestJob(request,
                network_delegate,
                net::URLRequestTestJob::test_headers(),
                std::string(),
                false)
        {
        }

        void GetResponseInfo(net::HttpResponseInfo* info) override
        {
            URLRequestTestJob::GetResponseInfo(info);
            if (request()->load_flags() & net::LOAD_SUPPORT_ASYNC_REVALIDATION)
                info->async_revalidation_required = true;
        }
    };

    // A URLRequestJob implementation which serves a redirect and sets the
    // |async_revalidation_required| flag on the HttpResponseInfo object to true if
    // the request has the LOAD_SUPPORT_ASYNC_REVALIDATION flag.
    class RedirectAndRevalidateURLRequestTestJob : public net::URLRequestTestJob {
    public:
        RedirectAndRevalidateURLRequestTestJob(net::URLRequest* request,
            net::NetworkDelegate* network_delegate)
            : URLRequestTestJob(request,
                network_delegate,
                CreateRedirectHeaders(),
                std::string(),
                false)
        {
        }

        void GetResponseInfo(net::HttpResponseInfo* info) override
        {
            URLRequestTestJob::GetResponseInfo(info);
            if (request()->load_flags() & net::LOAD_SUPPORT_ASYNC_REVALIDATION)
                info->async_revalidation_required = true;
        }

    private:
        static std::string CreateRedirectHeaders()
        {
            static const char kRedirectHeaders[] = "HTTP/1.1 302 MOVED\n"
                                                   "Location: http://example.com/async-revalidate/from-redirect\n"
                                                   "\n";
            return std::string(kRedirectHeaders, arraysize(kRedirectHeaders) - 1);
        }
    };

    class TestURLRequestJobFactory : public net::URLRequestJobFactory {
    public:
        TestURLRequestJobFactory() = default;

        // Sets the contents of the response. |headers| should have "\n" as line
        // breaks and end in "\n\n".
        void SetResponse(const std::string& headers, const std::string& data)
        {
            response_headers_ = headers;
            response_data_ = data;
        }

        net::URLRequestJob* MaybeCreateJobWithProtocolHandler(
            const std::string& scheme,
            net::URLRequest* request,
            net::NetworkDelegate* network_delegate) const override
        {
            std::string path = request->url().path();
            if (base::StartsWith(path, "/async-revalidate",
                    base::CompareCase::SENSITIVE)) {
                return new AsyncRevalidationRequiredURLRequestTestJob(request,
                    network_delegate);
            }
            if (base::StartsWith(path, "/redirect", base::CompareCase::SENSITIVE)) {
                return new RedirectAndRevalidateURLRequestTestJob(request,
                    network_delegate);
            }
            return new URLRequestTestDelayedCompletionJob(
                request, network_delegate, response_headers_, response_data_);
        }

        net::URLRequestJob* MaybeInterceptRedirect(
            net::URLRequest* request,
            net::NetworkDelegate* network_delegate,
            const GURL& location) const override
        {
            return nullptr;
        }

        net::URLRequestJob* MaybeInterceptResponse(
            net::URLRequest* request,
            net::NetworkDelegate* network_delegate) const override
        {
            return nullptr;
        }

        bool IsHandledProtocol(const std::string& scheme) const override
        {
            // If non-standard schemes need to be tested in future it will be
            // necessary to call ChildProcessSecurityPolicyImpl::
            // RegisterWebSafeScheme() for them.
            return scheme == url::kHttpScheme || scheme == url::kHttpsScheme;
        }

        bool IsHandledURL(const GURL& url) const override
        {
            return IsHandledProtocol(url.scheme());
        }

        bool IsSafeRedirectTarget(const GURL& location) const override
        {
            return false;
        }

    private:
        std::string response_headers_;
        std::string response_data_;

        DISALLOW_COPY_AND_ASSIGN(TestURLRequestJobFactory);
    };

    // On Windows, ResourceMsg_SetDataBuffer supplies a HANDLE which is not
    // automatically released.
    //
    // See ResourceDispatcher::ReleaseResourcesInDataMessage.
    //
    // TODO(ricea): Maybe share this implementation with
    // resource_dispatcher_host_unittest.cc.
    void ReleaseHandlesInMessage(const IPC::Message& message)
    {
        if (message.type() == ResourceMsg_SetDataBuffer::ID) {
            base::PickleIterator iter(message);
            int request_id;
            CHECK(iter.ReadInt(&request_id));
            base::SharedMemoryHandle shm_handle;
            if (IPC::ParamTraits<base::SharedMemoryHandle>::Read(&message, &iter,
                    &shm_handle)) {
                if (base::SharedMemory::IsHandleValid(shm_handle))
                    base::SharedMemory::CloseHandle(shm_handle);
            }
        }
    }

    // This filter just deletes any messages that are sent through it.
    class BlackholeFilter : public ResourceMessageFilter {
    public:
        explicit BlackholeFilter(ResourceContext* resource_context)
            : ResourceMessageFilter(
                ChildProcessHostImpl::GenerateChildProcessUniqueId(),
                nullptr,
                nullptr,
                nullptr,
                nullptr,
                base::Bind(&BlackholeFilter::GetContexts, base::Unretained(this)))
            , resource_context_(resource_context)
        {
            InitializeForTest();
            ChildProcessSecurityPolicyImpl::GetInstance()->Add(child_id());
        }

        bool Send(IPC::Message* msg) override
        {
            std::unique_ptr<IPC::Message> take_ownership(msg);
            ReleaseHandlesInMessage(*msg);
            return true;
        }

    private:
        ~BlackholeFilter() override
        {
            ChildProcessSecurityPolicyImpl::GetInstance()->Remove(child_id());
        }

        void GetContexts(ResourceType resource_type,
            ResourceContext** resource_context,
            net::URLRequestContext** request_context)
        {
            *resource_context = resource_context_;
            *request_context = resource_context_->GetRequestContext();
        }

        ResourceContext* resource_context_;

        DISALLOW_COPY_AND_ASSIGN(BlackholeFilter);
    };

    ResourceRequest CreateResourceRequest(const char* method,
        ResourceType type,
        const GURL& url)
    {
        ResourceRequest request;
        request.method = std::string(method);
        request.url = url;
        request.first_party_for_cookies = url; // Bypass third-party cookie blocking.
        request.request_initiator = url::Origin(url); // Ensure initiator is set.
        request.referrer_policy = blink::WebReferrerPolicyDefault;
        request.load_flags = 0;
        request.origin_pid = 0;
        request.resource_type = type;
        request.request_context = 0;
        request.appcache_host_id = kAppCacheNoHostId;
        request.download_to_file = false;
        request.should_reset_appcache = false;
        request.is_main_frame = true;
        request.parent_is_main_frame = false;
        request.parent_render_frame_id = -1;
        request.transition_type = ui::PAGE_TRANSITION_LINK;
        request.allow_download = true;
        return request;
    }

    class AsyncRevalidationManagerTest : public ::testing::Test {
    protected:
        AsyncRevalidationManagerTest(
            std::unique_ptr<net::TestNetworkDelegate> network_delegate)
            : thread_bundle_(content::TestBrowserThreadBundle::IO_MAINLOOP)
            , network_delegate_(std::move(network_delegate))
        {
            host_.SetLoaderDelegate(&loader_delegate_);
            browser_context_.reset(new TestBrowserContext());
            BrowserContext::EnsureResourceContextInitialized(browser_context_.get());
            base::RunLoop().RunUntilIdle();
            ResourceContext* resource_context = browser_context_->GetResourceContext();
            filter_ = new BlackholeFilter(resource_context);
            net::URLRequestContext* request_context = resource_context->GetRequestContext();
            job_factory_.reset(new TestURLRequestJobFactory);
            request_context->set_job_factory(job_factory_.get());
            request_context->set_network_delegate(network_delegate_.get());
            host_.EnableStaleWhileRevalidateForTesting();
        }

        AsyncRevalidationManagerTest()
            : AsyncRevalidationManagerTest(
                base::WrapUnique(new net::TestNetworkDelegate))
        {
        }

        void TearDown() override
        {
            filter_->OnChannelClosing();
            host_.CancelRequestsForProcess(filter_->child_id());
            host_.Shutdown();
            host_.CancelRequestsForContext(browser_context_->GetResourceContext());
            browser_context_.reset();
            base::RunLoop().RunUntilIdle();
        }

        void SetResponse(const std::string& headers, const std::string& data)
        {
            job_factory_->SetResponse(headers, data);
        }

        // Creates a request using the current test object as the filter and
        // SubResource as the resource type.
        void MakeTestRequest(int render_view_id, int request_id, const GURL& url)
        {
            ResourceRequest request = CreateResourceRequest("GET", RESOURCE_TYPE_SUB_RESOURCE, url);
            ResourceHostMsg_RequestResource msg(render_view_id, request_id, request);
            filter_->OnMessageReceived(msg);
            base::RunLoop().RunUntilIdle();
        }

        void EnsureSchemeIsAllowed(const std::string& scheme)
        {
            ChildProcessSecurityPolicyImpl* policy = ChildProcessSecurityPolicyImpl::GetInstance();
            if (!policy->IsWebSafeScheme(scheme))
                policy->RegisterWebSafeScheme(scheme);
        }

        content::TestBrowserThreadBundle thread_bundle_;
        std::unique_ptr<TestBrowserContext> browser_context_;
        std::unique_ptr<TestURLRequestJobFactory> job_factory_;
        scoped_refptr<BlackholeFilter> filter_;
        std::unique_ptr<net::TestNetworkDelegate> network_delegate_;
        LoaderDelegateImpl loader_delegate_;
        ResourceDispatcherHostImpl host_;
    };

    TEST_F(AsyncRevalidationManagerTest, SupportsAsyncRevalidation)
    {
        SetResponse(net::URLRequestTestJob::test_headers(), "delay complete");
        MakeTestRequest(0, 1, GURL("http://example.com/baz"));

        net::URLRequest* url_request(
            host_.GetURLRequest(GlobalRequestID(filter_->child_id(), 1)));
        ASSERT_TRUE(url_request);

        EXPECT_TRUE(url_request->load_flags() & net::LOAD_SUPPORT_ASYNC_REVALIDATION);
    }

    TEST_F(AsyncRevalidationManagerTest, AsyncRevalidationNotSupportedForPOST)
    {
        SetResponse(net::URLRequestTestJob::test_headers(), "delay complete");
        // Create POST request.
        ResourceRequest request = CreateResourceRequest(
            "POST", RESOURCE_TYPE_SUB_RESOURCE, GURL("http://example.com/baz.php"));
        ResourceHostMsg_RequestResource msg(0, 1, request);
        filter_->OnMessageReceived(msg);
        base::RunLoop().RunUntilIdle();

        net::URLRequest* url_request(
            host_.GetURLRequest(GlobalRequestID(filter_->child_id(), 1)));
        ASSERT_TRUE(url_request);

        EXPECT_FALSE(url_request->load_flags() & net::LOAD_SUPPORT_ASYNC_REVALIDATION);
    }

    TEST_F(AsyncRevalidationManagerTest,
        AsyncRevalidationNotSupportedAfterRedirect)
    {
        static const char kRedirectHeaders[] = "HTTP/1.1 302 MOVED\n"
                                               "Location: http://example.com/var\n"
                                               "\n";
        SetResponse(kRedirectHeaders, "");

        MakeTestRequest(0, 1, GURL("http://example.com/baz"));

        net::URLRequest* url_request(
            host_.GetURLRequest(GlobalRequestID(filter_->child_id(), 1)));
        ASSERT_TRUE(url_request);

        EXPECT_FALSE(url_request->load_flags() & net::LOAD_SUPPORT_ASYNC_REVALIDATION);
    }

    // A NetworkDelegate that records the URLRequests as they are created.
    class URLRequestRecordingNetworkDelegate : public net::TestNetworkDelegate {
    public:
        URLRequestRecordingNetworkDelegate()
            : requests_()
        {
        }

        net::URLRequest* NextRequest()
        {
            EXPECT_FALSE(requests_.empty());
            if (requests_.empty())
                return nullptr;
            net::URLRequest* request = requests_.front();
            EXPECT_TRUE(request);
            requests_.pop_front();
            return request;
        }

        bool NextRequestWasDestroyed()
        {
            net::URLRequest* request = requests_.front();
            requests_.pop_front();
            return request == nullptr;
        }

        bool IsEmpty() const { return requests_.empty(); }

        int OnBeforeURLRequest(net::URLRequest* request,
            const net::CompletionCallback& callback,
            GURL* new_url) override
        {
            requests_.push_back(request);
            return TestNetworkDelegate::OnBeforeURLRequest(request, callback, new_url);
        }

        void OnURLRequestDestroyed(net::URLRequest* request) override
        {
            for (auto*& recorded_request : requests_) {
                if (recorded_request == request)
                    recorded_request = nullptr;
            }
            net::TestNetworkDelegate::OnURLRequestDestroyed(request);
        }

    private:
        std::deque<net::URLRequest*> requests_;

        DISALLOW_COPY_AND_ASSIGN(URLRequestRecordingNetworkDelegate);
    };

    class AsyncRevalidationManagerRecordingTest
        : public AsyncRevalidationManagerTest {
    public:
        AsyncRevalidationManagerRecordingTest()
            : AsyncRevalidationManagerTest(
                base::WrapUnique(new URLRequestRecordingNetworkDelegate))
        {
        }

        void TearDown() override
        {
            EXPECT_TRUE(IsEmpty());
            AsyncRevalidationManagerTest::TearDown();
        }

        URLRequestRecordingNetworkDelegate* recording_network_delegate() const
        {
            return static_cast<URLRequestRecordingNetworkDelegate*>(
                network_delegate_.get());
        }

        bool NextRequestWasDestroyed()
        {
            return recording_network_delegate()->NextRequestWasDestroyed();
        }

        net::URLRequest* NextRequest()
        {
            return recording_network_delegate()->NextRequest();
        }

        bool IsEmpty() const { return recording_network_delegate()->IsEmpty(); }
    };

    // Verify that an async revalidation is actually created when needed.
    TEST_F(AsyncRevalidationManagerRecordingTest, Issued)
    {
        // Create the original request.
        MakeTestRequest(0, 1, GURL("http://example.com/async-revalidate"));

        net::URLRequest* initial_request = NextRequest();
        ASSERT_TRUE(initial_request);
        EXPECT_TRUE(initial_request->load_flags() & net::LOAD_SUPPORT_ASYNC_REVALIDATION);

        net::URLRequest* async_request = NextRequest();
        ASSERT_TRUE(async_request);
    }

    // Verify the the URL of the async revalidation matches the original request.
    TEST_F(AsyncRevalidationManagerRecordingTest, URLMatches)
    {
        // Create the original request.
        MakeTestRequest(0, 1, GURL("http://example.com/async-revalidate/u"));

        // Discard the original request.
        NextRequest();

        net::URLRequest* async_request = NextRequest();
        ASSERT_TRUE(async_request);
        EXPECT_EQ(GURL("http://example.com/async-revalidate/u"),
            async_request->url());
    }

    TEST_F(AsyncRevalidationManagerRecordingTest,
        AsyncRevalidationsDoNotSupportAsyncRevalidation)
    {
        // Create the original request.
        MakeTestRequest(0, 1, GURL("http://example.com/async-revalidate"));

        // Discard the original request.
        NextRequest();

        // Get the async revalidation request.
        net::URLRequest* async_request = NextRequest();
        ASSERT_TRUE(async_request);
        EXPECT_FALSE(async_request->load_flags() & net::LOAD_SUPPORT_ASYNC_REVALIDATION);
    }

    TEST_F(AsyncRevalidationManagerRecordingTest, AsyncRevalidationsNotDuplicated)
    {
        // Create the original request.
        MakeTestRequest(0, 1, GURL("http://example.com/async-revalidate"));

        // Discard the original request.
        NextRequest();

        // Get the async revalidation request.
        net::URLRequest* async_request = NextRequest();
        EXPECT_TRUE(async_request);

        // Start a second request to the same URL.
        MakeTestRequest(0, 2, GURL("http://example.com/async-revalidate"));

        // Discard the second request.
        NextRequest();

        // There should not be another async revalidation request.
        EXPECT_TRUE(IsEmpty());
    }

    // Async revalidation to different URLs should not be treated as duplicates.
    TEST_F(AsyncRevalidationManagerRecordingTest,
        AsyncRevalidationsToSeparateURLsAreSeparate)
    {
        // Create two requests to two URLs.
        MakeTestRequest(0, 1, GURL("http://example.com/async-revalidate"));
        MakeTestRequest(0, 2, GURL("http://example.com/async-revalidate2"));

        net::URLRequest* initial_request = NextRequest();
        ASSERT_TRUE(initial_request);
        net::URLRequest* initial_async_revalidation = NextRequest();
        ASSERT_TRUE(initial_async_revalidation);
        net::URLRequest* second_request = NextRequest();
        ASSERT_TRUE(second_request);
        net::URLRequest* second_async_revalidation = NextRequest();
        ASSERT_TRUE(second_async_revalidation);

        EXPECT_EQ("http://example.com/async-revalidate",
            initial_request->url().spec());
        EXPECT_EQ("http://example.com/async-revalidate",
            initial_async_revalidation->url().spec());
        EXPECT_EQ("http://example.com/async-revalidate2",
            second_request->url().spec());
        EXPECT_EQ("http://example.com/async-revalidate2",
            second_async_revalidation->url().spec());
    }

    // Nothing after the first redirect leg has stale-while-revalidate applied.
    // TODO(ricea): s-w-r should work with redirects. Change this test when it does.
    TEST_F(AsyncRevalidationManagerRecordingTest, NoSWRAfterFirstRedirectLeg)
    {
        MakeTestRequest(0, 1, GURL("http://example.com/redirect"));

        net::URLRequest* initial_request = NextRequest();
        EXPECT_TRUE(initial_request);

        EXPECT_FALSE(initial_request->load_flags() & net::LOAD_SUPPORT_ASYNC_REVALIDATION);

        // An async revalidation happens for the redirect. It has no body, so it has
        // already completed.
        EXPECT_TRUE(NextRequestWasDestroyed());

        // But no others.
        EXPECT_TRUE(IsEmpty());
    }

} // namespace

} // namespace content
